{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 互動式 Phi 3 Mini 4K 指導聊天機器人與 Whisper\n",
        "\n",
        "### 簡介:\n",
        "互動式 Phi 3 Mini 4K 指導聊天機器人是一個工具，允許使用者使用文字或音訊輸入與 Microsoft Phi 3 Mini 4K 指導展示進行互動。該聊天機器人可用於各種任務，例如翻譯、天氣更新和一般資訊收集。\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "id": "Atl_WEmtR0Yd"
      },
      "outputs": [],
      "source": [
        "#Install required Python Packages\n",
        "!pip install accelerate\n",
        "!pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118\n",
        "!pip install flash-attn --no-build-isolation', env={'FLASH_ATTENTION_SKIP_CUDA_BUILD': \"TRUE\"}, shell=True\n",
        "!pip install transformers\n",
        "!pip install wheel\n",
        "!pip install gradio\n",
        "!pip install pydub==0.25.1\n",
        "!pip install edge-tts\n",
        "!pip install openai-whisper==20231117\n",
        "!pip install ffmpeg==1.4\n",
        "# from IPython.display import clear_output\n",
        "# clear_output()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Checking to see if Cuda support is available \n",
        "# Output True = Cuda\n",
        "# Output False = No Cuda (installing Cuda will be required to run the model on GPU)\n",
        "import os \n",
        "import torch\n",
        "print(torch.cuda.is_available())\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "MKAUp20H4ZXl"
      },
      "source": [
        "[Create your Huggingface Access Token](https://huggingface.co/settings/tokens)\n",
        "\n",
        "建立一個新的 token \n",
        "提供一個新名稱 \n",
        "選擇寫入權限\n",
        "複製 token 並將其保存在安全的地方\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "以下 Python 程式碼執行兩個主要任務: 匯入 `os` 模組和設定環境變數。\n",
        "\n",
        "1. 匯入 `os` 模組:\n",
        "   - Python 中的 `os` 模組提供了一種與作業系統互動的方式。它允許你執行各種與作業系統相關的任務，例如存取環境變數、操作檔案和目錄等。\n",
        "   - 在這段程式碼中，使用 `import` 語句匯入 `os` 模組。這個語句使 `os` 模組的功能可以在當前的 Python 腳本中使用。\n",
        "\n",
        "2. 設定環境變數:\n",
        "   - 環境變數是可以被作業系統上執行的程式存取的值。它是一種儲存配置設定或其他資訊的方式，可以被多個程式使用。\n",
        "   - 在這段程式碼中，使用 `os.environ` 字典設定了一個新的環境變數。字典的鍵是 `'HF_TOKEN'`，值是從 `HUGGINGFACE_TOKEN` 變數中分配的。\n",
        "   - `HUGGINGFACE_TOKEN` 變數定義在這段程式碼片段的上方，並使用 `#@param` 語法分配了一個字串值 `\"hf_**** **** **** **\"`。這種語法通常在 Jupyter 筆記本中使用，允許使用者在筆記本介面中直接輸入和配置參數。\n",
        "   - 通過設定 `'HF_TOKEN'` 環境變數，它可以被程式的其他部分或在相同作業系統上執行的其他程式存取。\n",
        "\n",
        "總的來說，這段程式碼匯入了 `os` 模組並設定了一個名為 `'HF_TOKEN'` 的環境變數，其值來自 `HUGGINGFACE_TOKEN` 變數。\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "id": "N5r2ikbwR68c"
      },
      "outputs": [],
      "source": [
        "import os\n",
        "# set the Hugging Face Token from \n",
        "# add the Hugging Face Token to the environment variables\n",
        "HUGGINGFACE_TOKEN = \"Enter Hugging Face Key\" #@param {type:\"string\"}\n",
        "os.environ['HF_TOKEN']HUGGINGFACE_TOKEN"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "這段程式碼片段定義了一個名為 clear_output 的函式，用於清除 Jupyter Notebook 或 IPython 中當前單元格的輸出。讓我們拆解這段程式碼並理解其功能：\n",
        "\n",
        "函式 clear_output 接受一個名為 wait 的參數，這是一個布林值。預設情況下，wait 設為 False。這個參數決定了函式是否應該等待直到有新的輸出可用來替換現有輸出再清除它。\n",
        "\n",
        "這個函式本身用於清除當前單元格的輸出。在 Jupyter Notebook 或 IPython 中，當一個單元格產生輸出時，例如打印的文字或圖形繪圖，該輸出會顯示在單元格下方。clear_output 函式允許你清除該輸出。\n",
        "\n",
        "程式碼片段中沒有提供函式的實現，如省略號（...）所示。省略號表示實際執行清除輸出的程式碼的佔位符。函式的實現可能涉及與 Jupyter Notebook 或 IPython API 互動，以從單元格中移除現有輸出。\n",
        "\n",
        "總的來說，這個函式提供了一種方便的方法來清除 Jupyter Notebook 或 IPython 中當前單元格的輸出，使得在互動式編碼會話期間更容易管理和更新顯示的輸出。\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "id": "nmXm0dxuRinA"
      },
      "outputs": [],
      "source": [
        "# Download Phi-3-mini-4k-instruct model & Whisper Tiny\n",
        "import torch\n",
        "from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline\n",
        "\n",
        "torch.random.manual_seed(0)\n",
        "\n",
        "model = AutoModelForCausalLM.from_pretrained(\n",
        "    \"microsoft/Phi-3-mini-4k-instruct\",\n",
        "    device_map=\"cuda\",\n",
        "    torch_dtype=\"auto\",\n",
        "    trust_remote_code=True,\n",
        ")\n",
        "tokenizer = AutoTokenizer.from_pretrained(\"microsoft/Phi-3-mini-4k-instruct\")\n",
        "\n",
        "#whisper for speech to text()\n",
        "import whisper\n",
        "select_model =\"tiny\" # ['tiny', 'base']\n",
        "whisper_model = whisper.load_model(select_model)\n",
        "\n",
        "#from IPython.display import clear_output\n",
        "#clear_output()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "執行文字轉語音（TTS）使用 Edge TTS 服務。讓我們逐一了解相關的函式實作:\n",
        "\n",
        "1. `calculate_rate_string(input_value)`：此函式接收一個輸入值並計算 TTS 聲音的速率字串。輸入值代表所需的語速，其中值為 1 代表正常速度。函式通過從輸入值中減去 1，將其乘以 100，然後根據輸入值是否大於或等於 1 來確定符號。函式以\"{sign}{rate}\"格式返回速率字串。\n",
        "\n",
        "2. `make_chunks(input_text, language)`：此函式接收輸入文字和語言作為參數。它根據語言特定的規則將輸入文字分割成塊。在此實作中，如果語言是\"English\"，函式會在每個句號（\".\"）處分割文字並移除任何前導或尾隨的空白。然後，它會在每個塊後附加一個句號並返回過濾後的塊列表。\n",
        "\n",
        "3. `tts_file_name(text)`：此函式根據輸入文字生成 TTS 音訊檔案的檔名。它對文字進行多次轉換：移除尾隨的句號（如果存在），將文字轉換為小寫，去除前導和尾隨的空白，並將空格替換為底線。然後，它將文字截斷為最多 25 個字元（如果更長）或使用完整文字（如果為空）。最後，它使用[`uuid`]模組生成一個隨機字串，並將其與截斷的文字結合以建立檔名，格式為\"/content/edge_tts_voice/{truncated_text}_{random_string}.mp3\"。\n",
        "\n",
        "4. `merge_audio_files(audio_paths, output_path)`：此函式將多個音訊檔案合併為一個音訊檔案。它接收一個音訊檔案路徑列表和一個輸出路徑作為參數。函式初始化一個空的`AudioSegment`物件，稱為[`merged_audio`]。然後，它遍歷每個音訊檔案路徑，使用`pydub`函式庫的`AudioSegment.from_file()`方法載入音訊檔案，並將當前音訊檔案附加到[`merged_audio`]物件。最後，它將合併的音訊以 MP3 格式匯出到指定的輸出路徑。\n",
        "\n",
        "5. `edge_free_tts(chunks_list, speed, voice_name, save_path)`：此函式使用 Edge TTS 服務執行 TTS 操作。它接收一個文字塊列表、語速、聲音名稱和儲存路徑作為參數。如果塊的數量大於 1，函式會為儲存個別塊音訊檔案建立一個目錄。然後，它遍歷每個塊，使用`calculate_rate_string()`函式、聲音名稱和塊文字構建一個 Edge TTS 命令，並使用`os.system()`函式執行該命令。如果命令執行成功，它會將生成的音訊檔案路徑附加到列表中。處理完所有塊後，它使用`merge_audio_files()`函式合併個別音訊檔案並將合併的音訊儲存到指定的儲存路徑。如果只有一個塊，它會直接生成 Edge TTS 命令並將音訊儲存到儲存路徑。最後，它返回生成的音訊檔案的儲存路徑。\n",
        "\n",
        "6. `random_audio_name_generate()`：此函式使用[`uuid`]模組生成一個隨機音訊檔案名稱。它生成一個隨機的 UUID，將其轉換為字串，取前 8 個字元，附加\".mp3\"擴展名，並返回隨機音訊檔案名稱。\n",
        "\n",
        "7. `talk(input_text)`：此函式是執行 TTS 操作的主要入口點。它接收一個輸入文字作為參數。它首先檢查輸入文字的長度以確定是否為長句子（大於或等於 600 個字元）。根據長度和`translate_text_flag`變數的值，它確定語言並使用`make_chunks()`函式生成文字塊列表。然後，它使用`random_audio_name_generate()`函式生成音訊檔案的儲存路徑。最後，它呼叫`edge_free_tts()`函式執行 TTS 操作並返回生成的音訊檔案的儲存路徑。\n",
        "\n",
        "總體而言，這些函式共同作用以將輸入文字分割成塊，生成音訊檔案的檔名，使用 Edge TTS 服務執行 TTS 操作，並將個別音訊檔案合併為一個音訊檔案。\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 93
        },
        "id": "Mv4WVhNUz4IL",
        "outputId": "7f177f73-3eb1-4d7c-d5e9-1e7cabe32f63"
      },
      "outputs": [],
      "source": [
        "#@title Edge TTS\n",
        "def calculate_rate_string(input_value):\n",
        "    rate = (input_value - 1) * 100\n",
        "    sign = '+' if input_value >= 1 else '-'\n",
        "    return f\"{sign}{abs(int(rate))}\"\n",
        "\n",
        "\n",
        "def make_chunks(input_text, language):\n",
        "    language=\"English\"\n",
        "    if language == \"English\":\n",
        "      temp_list = input_text.strip().split(\".\")\n",
        "      filtered_list = [element.strip() + '.' for element in temp_list[:-1] if element.strip() and element.strip() != \"'\" and element.strip() != '\"']\n",
        "      if temp_list[-1].strip():\n",
        "          filtered_list.append(temp_list[-1].strip())\n",
        "      return filtered_list\n",
        "\n",
        "\n",
        "import re\n",
        "import uuid\n",
        "def tts_file_name(text):\n",
        "    if text.endswith(\".\"):\n",
        "        text = text[:-1]\n",
        "    text = text.lower()\n",
        "    text = text.strip()\n",
        "    text = text.replace(\" \",\"_\")\n",
        "    truncated_text = text[:25] if len(text) > 25 else text if len(text) > 0 else \"empty\"\n",
        "    random_string = uuid.uuid4().hex[:8].upper()\n",
        "    file_name = f\"/content/edge_tts_voice/{truncated_text}_{random_string}.mp3\"\n",
        "    return file_name\n",
        "\n",
        "\n",
        "from pydub import AudioSegment\n",
        "import shutil\n",
        "import os\n",
        "def merge_audio_files(audio_paths, output_path):\n",
        "    # Initialize an empty AudioSegment\n",
        "    merged_audio = AudioSegment.silent(duration=0)\n",
        "\n",
        "    # Iterate through each audio file path\n",
        "    for audio_path in audio_paths:\n",
        "        # Load the audio file using Pydub\n",
        "        audio = AudioSegment.from_file(audio_path)\n",
        "\n",
        "        # Append the current audio file to the merged_audio\n",
        "        merged_audio += audio\n",
        "\n",
        "    # Export the merged audio to the specified output path\n",
        "    merged_audio.export(output_path, format=\"mp3\")\n",
        "\n",
        "def edge_free_tts(chunks_list,speed,voice_name,save_path):\n",
        "  # print(chunks_list)\n",
        "  if len(chunks_list)>1:\n",
        "    chunk_audio_list=[]\n",
        "    if os.path.exists(\"/content/edge_tts_voice\"):\n",
        "      shutil.rmtree(\"/content/edge_tts_voice\")\n",
        "    os.mkdir(\"/content/edge_tts_voice\")\n",
        "    k=1\n",
        "    for i in chunks_list:\n",
        "      print(i)\n",
        "      edge_command=f'edge-tts  --rate={calculate_rate_string(speed)}% --voice {voice_name} --text \"{i}\" --write-media /content/edge_tts_voice/{k}.mp3'\n",
        "      print(edge_command)\n",
        "      var1=os.system(edge_command)\n",
        "      if var1==0:\n",
        "        pass\n",
        "      else:\n",
        "        print(f\"Failed: {i}\")\n",
        "      chunk_audio_list.append(f\"/content/edge_tts_voice/{k}.mp3\")\n",
        "      k+=1\n",
        "    # print(chunk_audio_list)\n",
        "    merge_audio_files(chunk_audio_list, save_path)\n",
        "  else:\n",
        "    edge_command=f'edge-tts  --rate={calculate_rate_string(speed)}% --voice {voice_name} --text \"{chunks_list[0]}\" --write-media {save_path}'\n",
        "    print(edge_command)\n",
        "    var2=os.system(edge_command)\n",
        "    if var2==0:\n",
        "      pass\n",
        "    else:\n",
        "      print(f\"Failed: {chunks_list[0]}\")\n",
        "  return save_path\n",
        "\n",
        "# text = \"This is Microsoft Phi 3 mini 4k instruct Demo\" Simply update the text variable with the text you want to convert to speech\n",
        "text = 'This is Microsoft Phi 3 mini 4k instruct Demo'  # @param {type: \"string\"}\n",
        "Language = \"English\" # @param ['English']\n",
        "# Gender of voice simply change from male to female and choose the voice you want to use\n",
        "Gender = \"Female\"# @param ['Male', 'Female']\n",
        "female_voice=\"en-US-AriaNeural\"# @param[\"en-US-AriaNeural\",'zh-CN-XiaoxiaoNeural','zh-CN-XiaoyiNeural']\n",
        "speed = 1  # @param {type: \"number\"}\n",
        "translate_text_flag  = False\n",
        "if len(text)>=600:\n",
        "  long_sentence = True\n",
        "else:\n",
        "  long_sentence = False\n",
        "\n",
        "# long_sentence = False # @param {type:\"boolean\"}\n",
        "save_path = ''  # @param {type: \"string\"}\n",
        "if len(save_path)==0:\n",
        "  save_path=tts_file_name(text)\n",
        "if Language == \"English\" :\n",
        "  if Gender==\"Male\":\n",
        "    voice_name=\"en-US-ChristopherNeural\"\n",
        "  if Gender==\"Female\":\n",
        "    voice_name=female_voice\n",
        "    # voice_name=\"en-US-AriaNeural\"\n",
        "\n",
        "\n",
        "if translate_text_flag:\n",
        "  input_text=text\n",
        "  # input_text=translate_text(text, Language)\n",
        "  # print(\"Translateting\")\n",
        "else:\n",
        "  input_text=text\n",
        "if long_sentence==True and translate_text_flag==True:\n",
        "  chunks_list=make_chunks(input_text,Language)\n",
        "elif long_sentence==True and translate_text_flag==False:\n",
        "  chunks_list=make_chunks(input_text,\"English\")\n",
        "else:\n",
        "  chunks_list=[input_text]\n",
        "# print(chunks_list)\n",
        "# edge_save_path=edge_free_tts(chunks_list,speed,voice_name,save_path)\n",
        "# from IPython.display import clear_output\n",
        "# clear_output()\n",
        "# from IPython.display import Audio\n",
        "# Audio(edge_save_path, autoplay=True)\n",
        "\n",
        "from IPython.display import clear_output\n",
        "from IPython.display import Audio\n",
        "if not os.path.exists(\"/content/audio\"):\n",
        "    os.mkdir(\"/content/audio\")\n",
        "import uuid\n",
        "def random_audio_name_generate():\n",
        "  random_uuid = uuid.uuid4()\n",
        "  audio_extension = \".mp3\"\n",
        "  random_audio_name = str(random_uuid)[:8] + audio_extension\n",
        "  return random_audio_name\n",
        "def talk(input_text):\n",
        "  global translate_text_flag,Language,speed,voice_name\n",
        "  if len(input_text)>=600:\n",
        "    long_sentence = True\n",
        "  else:\n",
        "    long_sentence = False\n",
        "\n",
        "  if long_sentence==True and translate_text_flag==True:\n",
        "    chunks_list=make_chunks(input_text,Language)\n",
        "  elif long_sentence==True and translate_text_flag==False:\n",
        "    chunks_list=make_chunks(input_text,\"English\")\n",
        "  else:\n",
        "    chunks_list=[input_text]\n",
        "  save_path=\"/content/audio/\"+random_audio_name_generate()\n",
        "  edge_save_path=edge_free_tts(chunks_list,speed,voice_name,save_path)\n",
        "  return edge_save_path\n",
        "\n",
        "\n",
        "edge_save_path=talk(text)\n",
        "Audio(edge_save_path, autoplay=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "實作了兩個函式：convert_to_text 和 run_text_prompt，以及宣告了兩個類別：str 和 Audio。\n",
        "\n",
        "convert_to_text 函式接受一個 audio_path 作為輸入，並使用一個名為 whisper_model 的模型將音訊轉錄為文字。該函式首先檢查 gpu 標誌是否設置為 True。如果是，則使用 whisper_model 並設置某些參數，例如 word_timestamps=True、fp16=True、language='English' 和 task='translate'。如果 gpu 標誌為 False，則使用 whisper_model 並設置 fp16=False。然後將轉錄結果保存到名為 'scan.txt' 的文件中，並返回作為文字。\n",
        "\n",
        "run_text_prompt 函式接受一個訊息和一個 chat_history 作為輸入。它使用 phi_demo 函式根據輸入訊息從聊天機器人生成回應。生成的回應然後傳遞給 talk 函式，該函式將回應轉換為音訊文件並返回文件路徑。Audio 類別用於顯示和播放音訊文件。音訊使用 IPython.display 模組中的 display 函式顯示，並使用 autoplay=True 參數創建 Audio 物件，因此音訊會自動開始播放。chat_history 更新為輸入訊息和生成的回應，並返回一個空字串和更新的 chat_history。\n",
        "\n",
        "str 類別是 Python 中的一個內建類別，表示一個字符序列。它提供了多種方法來操作和處理字串，例如 capitalize、casefold、center、count、encode、endswith、expandtabs、find、format、index、isalnum、isalpha、isascii、isdecimal、isdigit、isidentifier、islower、isnumeric、isprintable、isspace、istitle、isupper、join、ljust、lower、lstrip、partition、replace、removeprefix、removesuffix、rfind、rindex、rjust、rpartition、rsplit、rstrip、split、splitlines、startswith、strip、swapcase、title、translate、upper、zfill 等等。這些方法允許你執行搜尋、替換、格式化和操作字串等操作。\n",
        "\n",
        "Audio 類別是一個自定義類別，表示一個音訊物件。它用於在 Jupyter Notebook 環境中創建音訊播放器。該類別接受多種參數，例如 data、filename、url、embed、rate、autoplay 和 normalize。data 參數可以是 numpy 陣列、樣本列表、表示文件名或 URL 的字串，或原始 PCM 資料。filename 參數用於指定從本地文件加載音訊資料，url 參數用於指定下載音訊資料的 URL。embed 參數決定音訊資料是應該使用資料 URI 嵌入還是從原始來源引用。rate 參數指定音訊資料的取樣率。autoplay 參數決定音訊是否應該自動開始播放。normalize 參數指定音訊資料是否應該歸一化（重新縮放）到最大可能範圍。Audio 類別還提供了像 reload 這樣的方法來重新加載來自文件或 URL 的音訊資料，以及像 src_attr、autoplay_attr 和 element_id_attr 這樣的屬性來檢索 HTML 中音訊元素的相應屬性。\n",
        "\n",
        "總體而言，這些函式和類別用於將音訊轉錄為文字、從聊天機器人生成音訊回應，以及在 Jupyter Notebook 環境中顯示和播放音訊。\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "0e6aTA6mk7Gi",
        "outputId": "4c4825c9-f1ef-4d9e-d294-83d67248e073"
      },
      "outputs": [],
      "source": [
        "#@title Run gradio app\n",
        "def convert_to_text(audio_path):\n",
        "  gpu=True\n",
        "  if gpu:\n",
        "    result = whisper_model.transcribe(audio_path,word_timestamps=True,fp16=True,language='English',task='translate')\n",
        "  else:\n",
        "    result = whisper_model.transcribe(audio_path,word_timestamps=True,fp16=False,language='English',task='translate')\n",
        "  with open('scan.txt', 'w') as file:\n",
        "    file.write(str(result))\n",
        "  return result[\"text\"]\n",
        "\n",
        "\n",
        "import gradio as gr\n",
        "from IPython.display import Audio, display\n",
        "def run_text_prompt(message, chat_history):\n",
        "    bot_message = phi_demo(message)\n",
        "    edge_save_path=talk(bot_message)\n",
        "    # print(edge_save_path)\n",
        "    display(Audio(edge_save_path, autoplay=True))\n",
        "\n",
        "    chat_history.append((message, bot_message))\n",
        "    return \"\", chat_history\n",
        "\n",
        "\n",
        "def run_audio_prompt(audio, chat_history):\n",
        "    if audio is None:\n",
        "        return None, chat_history\n",
        "    print(audio)\n",
        "    message_transcription = convert_to_text(audio)\n",
        "    _, chat_history = run_text_prompt(message_transcription, chat_history)\n",
        "    return None, chat_history\n",
        "\n",
        "\n",
        "with gr.Blocks() as demo:\n",
        "    chatbot = gr.Chatbot(label=\"Chat with Phi 3 mini 4k instruct\")\n",
        "\n",
        "    msg = gr.Textbox(label=\"Ask anything\")\n",
        "    msg.submit(run_text_prompt, [msg, chatbot], [msg, chatbot])\n",
        "\n",
        "    with gr.Row():\n",
        "        audio = gr.Audio(sources=\"microphone\", type=\"filepath\")\n",
        "\n",
        "        send_audio_button = gr.Button(\"Send Audio\", interactive=True)\n",
        "        send_audio_button.click(run_audio_prompt, [audio, chatbot], [audio, chatbot])\n",
        "\n",
        "demo.launch(share=True,debug=True)"
      ]
    }
  ],
  "metadata": {
    "accelerator": "GPU",
    "colab": {
      "gpuType": "T4",
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.11.9"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}